<?php
/**
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 * 
 * @author Eddie Drapkin <eddie@frameweld.com>
 * @license http://www.gnu.org/licenses/lgpl-3.0.txt
 * @package Solphr
 * @version 0.1-pre
 */

require_once dirname(__FILE__) . '/Solphr_Exception.php';
require_once dirname(__FILE__) . '/Request/Solphr_Request_Interface.php';
require_once dirname(__FILE__) . '/Request/Solphr_Request_Curl.php';
require_once dirname(__FILE__) . '/Query/Solphr_Query_Interface.php';
require_once dirname(__FILE__) . '/Query/Solphr_Query_DateRange.php';
require_once dirname(__FILE__) . '/Query/Solphr_Query_Raw.php';
require_once dirname(__FILE__) . '/Query/Solphr_Query_Term.php';

/**
 * The primary class used for searching Solr.
 *
 * @author Eddie Drapkin <eddie@frameweld.com>
 * @license http://www.gnu.org/licenses/lgpl-3.0.txt
 * @package Solphr
 */
class Solphr_Search {
	/**
	 * Place holder used for grouped search results in the event of 
	 * the grouped column not existing for a particular document.
	 * 
	 * A theoretical result would look something like
	 * array(
	 * 		'value1' => array( $arrayOfResults ),
	 * 		'value2' => array( $arrayOfResults ),
	 * 		Solphr_Search::COLUMN_NOT_EXISTS => array( $arrayOfResults )
	 * );
	 * @var unknown_type
	 */
	const COLUMN_NOT_EXISTS = 0;
	
	/**
	 * JSON fetch mode.
	 */
	const FETCH_JSON = 1;
	/**
	 * Native PHP array fetch mode.
	 */
	const FETCH_ARRAY = 2;
	/**
	 * Grouped results fetch mode.
	 */
	const FETCH_GROUP = 4;
	/**
	 * Fetch into new classes.
	 */
	const FETCH_CLASS = 8;
	
	/**
	 * Version of Solr in use, for compatibility.
	 */
	const SOLR_VERS = 1.3;
	
	/**
	 * Option: Field to sort on.
	 */
	const OPT_SORTFIELD = 1000;
	/**
	 * Option: Direction to sort.
	 */
	const OPT_SORTwAY = 1001;
	/**
	 * Option: Fetch mode to use when not explicitly specified
	 * for a fetching operation.
	 */
	const OPT_FETCHMODE = 1002;
	/**
	 * Option: JSON named list treatment.
	 */
	const OPT_NL = 1003;
	/**
	 * Option: response writer. In nearly all cases, this shouldn't be changed.
	 */
	const OPT_WRITER = 1004;
	
	/**
	 * Sort ascending.
	 */
	const SORT_ASC = 2000;
	/**
	 * Sort descending.
	 */
	const SORT_DESC = 2001;
	
	/**
	 * Mapped named list treatment.
	 */
	const NL_MAP = 3000;
	/**
	 * Flat named list treatment.
	 */
	const NL_FLAT = 3001;
	
	/**
	 * Native PHP (var_export() format) response writer.
	 */
	const WRITER_PHP = 'PHP';
	/**
	 * Serialized PHP response writer.
	 */
	const WRITER_PHPS = 'PHPS';
	/**
	 * JSON response writer.
	 */
	const WRITER_JSON = 'JSON';
	/**
	 * XML response writer.  Changes the way fetch and fetchALl
	 * behaves as the entire result is returned as a single
	 * SimpleXML object.  Not recommended to be used.
	 */
	const WRITER_XML = 'XML';
	
	private $req;
	private $queryItems = array();
	private $sortFields = array();
	
	private $writer = 'JSON';
	private $start = 0;
	private $limit = 15;
	private $fetchMode = self::FETCH_ARRAY;
	private $nl = 'map';
	
	private $results;
	private $stats;
	

	/**
	 * Class constructor.
	 * @param string $URL The URL of Solr.
	 * @param int $httpMethod One of the Solr_Request_Interface::(GET|POST) HTTP method constants.
	 */
	public function __construct($URL, $httpMethod = Solphr_Request_Interface::GET) {
		$class = Solphr_Request_Interface::IMPL;
		$this->req = new $class($URL, $httpMethod);
	}

	/**
	 * Adds another field to sort by.  Field sort precedence is in the order they are added.
	 * @param string $field The field to sort on.
	 * @param int $direction The direction to sort by.  One of the SORT_ASC / SORT_DESC class constants.
	 * @return Solphr_Search The instance in use to allow for method chaining.
	 */
	public function addSortField($field, $direction) {
		if($direction !== self::SORT_ASC && $direction !== self::SORT_DESC) {
			throw new Solphr_Exception('Unexpected value for sort option.  Please use one of the class SORT_* constants.');
		} 
		
		$this->sortFields[] = array(
			'field' => $field,
			'direction' => $direction 
		);
	}
	
	/**
	 * Sets one of the class options.
	 * @param int $option One of the class OPT_* constants.
	 * @param mixed $value The corresponding value for the specific option
	 * @return Solphr_Search The instance in use to allow for method chaining.
	 */
	public function setOption($option, $value) {
		switch($option) {
	
			case self::OPT_FETCHMODE:
				$this->fetchMode = (int)$value;
				break;
				
			case self::OPT_NL:
				if($value !== self::NL_MAP && $value !== self::NL_FLAT) {
					throw new Solphr_Exception('Unexpected value for NL treatment.  Please use one of the class NL_* constants.');
				}
				$this->nl = $value;
				break;
			
			case self::OPT_WRITER:
				switch(strtoupper($value)) {
					case self::WRITER_JSON:
						$this->writer = 'JSON';
						break;
					case self::WRITER_PHP:
						$this->writer = 'PHP';
						break;
					case self::WRITER_PHPS:
						$this->writer = 'PHPS';
						break;
					case self::WRITER_XML:
						$this->writer = 'XML';
						break;
					default:
						throw new Solphr_Exception("Unsupported Solr response writer: $value");
				}
				
				break;
			default:
				throw new Solphr_Exception('Unsupported or unknown option.');
		}
		
		return $this;
	}
	
	/**
	 * Adds a query item to an instance.  This is an instance that corresponds to a
	 * filter to be placed on the index. 
	 * @param Solphr_Query_Interface $query
	 * @return Solphr_Search The instance in use to allow for method chaining.
	 */
	public function addQueryItem(Solphr_Query_Interface $query) {
		$this->queryItems[] = (string)$query;
		return $this;
	}
	
	/**
	 * Sets the search limits
	 * @param int $limit The maximum number of results to return.
	 * @param int $offset Offset of searching.
	 * @return Solphr_Search The instance in use to allow for method chaining. 
	 */
	public function setLimit($limit, $offset) {
		$this->limit = intval($limit);
		$this->start = intval($offset);
		
		return $this;
	}
	
	
	/**
	 * Fetches the next result.
	 * @param int $fetchMode The mode (one of the FETCH_* constants) to fetch.
	 * @return mixed the next result.
	 */
	public function fetch($fetchMode = null, $extraInfo = null) {
		if(!isset($fetchMode)) {
			$fetchMode = $this->fetchMode;
		}
		
		if($this->results instanceof SimpleXMLElement) {
			trigger_error('Fetch is not supported when using XML, entire set returned.', E_NOTICE);
			return $this->results;
		}
		
		$result = current($this->results['response']['docs']);
		next($this->results['response']['docs']);
		
		if($fetchMode & self::FETCH_GROUP) {
			$this->groupResults($extraInfo);
		}
		
		if($fetchMode & self::FETCH_ARRAY) {
			return $result;
		}
		
		if($fetchMode & self::FETCH_JSON) {
			return json_encode($result);
		}
		
		throw new Solphr_Exception('Unsupported fetch mode in use.');
	}
	
	/**
	 * Returns the entire result set.
	 * @param int $fetchMode The mode (one of the FETCH_* constants) to fetch.
	 * @return mixed The entire result set.
	 */
	public function fetchAll($fetchMode, $extraInfo = null) {
		if(!isset($fetchMode)) {
			$fetchMode = $this->fetchMode;
		}
		
		if($this->results instanceof SimpleXMLElement) {
			trigger_error('Fetch is not supported when using XML, entire set returned.', E_NOTICE);
			return $this->results;
		}
		
		if($fetchMode & self::FETCH_GROUP) {
			$this->groupResults($extraInfo);
		}
		
		if($fetchMode & self::FETCH_ARRAY) {
			return $this->results['response']['docs'];
		}
		
		if($fetchMode & self::FETCH_JSON) {
			return json_encode($this->results['response']['docs']);
		}

		throw new Solphr_Exception('Unsupported fetch mode in use.');
	}
	
	/**
	 * Returns the entire reply from Solr.  Note that this is different from ::fetchALl
	 * because fetchAll does NOT return the 'responseHeader' and extra information, such as
	 * numFound and maxScore.
	 * @return mixed The reply from Solr in the format it was requested.
	 */
	public function getAllResults() {
		return $this->results;
	}
	
	/**
	 * Returns our custom stats array.
	 * @return array The stats for the search results.
	 */
	public function getSearchStats() {
		return $this->stats;
	}
	
	/**
	 * Returns the number of results found.  Used as a utility/helper method when the full 
	 * stats array isn't working.
	 * @return int The number of results for the last search.
	 */
	public function numFound() {
		return $this->stats['numFound'];
	}
	
	
	/**
	 * Executes the current operation based on the parameters set in the class instance.
	 * @param array $extraParameters Extra parameters to be sent to Solr.
	 * @return bool Success or failure.
	 */
	public function execute(array $extraParameters = array()) {
		$parameters = array(
			'q' => implode('&', $this->queryItems),
			'version' => self::SOLR_VERS,
			'json.nl' => $this->nl,
			'rows' => $this->limit,
			'start' => $this->start,
			'wt' => $this->writer
		);
		
		if(($sort = $this->makeSort()) !== false) {
			$parameters['sort'] = $sort;
		}
		
		$parameters = array_merge($parameters, $extraParameters);
		
		$this->req->setDataFromArray($parameters);
		
		$results = $this->req->commit();
		
		if($results !== false) {

			switch(strtoupper($this->writer)) {
				case 'XML':
					$this->results = simplexml_load_string($results);
					$this->stats = array(
						'start' => (int)$this->results->response['start'],
						'status' => (bool)$this->results->responseHeader->status,
						'params' => $this->results->responseHeader->params,
						'offset' => (int)$this->results->response['start'],
						'results' => count($this->results->response),
						'maxScore' => (float)$this->results->response['maxScore'],
						'numFound' => (int)$this->results->response['numFound'],
						'queryTime' => (float)$this->results->responseHeader->QTime
					);
					break;
				case 'JSON':
					$this->results = json_decode($results, true);
					$this->parseStats();
					break;
				case 'PHP':
					trigger_error('PHP support requires eval() and is not recommended.  Please use a better writer format.', E_NOTICE);
					$this->results = eval('$this->results = ' . $results . ';');
					$this->parseStats();
					break;
				case 'PHPS':
					$this->results = unserialize($results);
					$this->parseStats();
					break;
				case 'PYTHON':
				case 'RUBY':
					throw new Solphr_Exception('Pretty sure you\'re using the wrong language there, bud.');
					break;
				default:
					throw new Solphr_Exception('Uknown writer format in use');
					break;
			}
		
		}
		
		return (bool)$this->results;
	}
	
	private function parseStats() {
		$this->stats = array(
			'numFound' => (int)$this->results['response']['numFound'],
			'start' => (int)$this->results['response']['start'],
			'offset' => (int)$this->results['response']['start'],
			'results' => count($this->results['response']['docs']),
			'maxScore' => (int)$this->results['response']['maxScore'],
			'params' => $this->results['responseHeader']['params'],
			'queryTime' => (float)$this->results['responseHeader']['QTime'],
			'status' => (bool)$this->results['responseHeader']['status']
		);

	}
	
	private function groupResults($columnName = null) {
		if(!isset($columnName)) {
			throw new Solphr_Exception('Must specify which field to group by, as field return presence or order is not necessarily guaranteed.');  
		}
		
		$tempResults = array();
		
		foreach($this->results['response']['docs'] as $result) {
			if(!isset($result[$columnName])) {
				$tempResults[self::COLUMN_NOT_EXISTS][] = $result;
			} else {
				$tempResults[(string)$result[$columnName]][] = $result;
			}
		}
		
		$this->results['response']['docs'] = $tempResults;
	}
	
	private function makeSort() {
		if(empty($this->sortFields)) {
			return false;
		}
		
		$qs = '';
		
		foreach($this->sortFields as $sort) {
			if(!empty($qs)) {
				$qs .= ', ';
			}
			
			$qs .= "{$sort['field']} ";
			
			if($sort['direction'] === self::SORT_ASC) {
				$qs .= "asc";
			} elseif($sort['direction'] === self::SORT_DEC) {
				$qs .= "desc";
			}
		}
		
		$this->sortFields = array();
		return $qs;
	}
	
	
}